FE

Webpack

前端的发展

模块化

把一个复杂的系统分解到多个模块以方便编码

CommonJS

  1. CommonJS 是一种使用广泛的 JavaScript 模块化规范,核心思想是通过 require 方法来同步地加载依赖的其他模块,通过 module.exports 导出需要暴露的接口.

  2. CommonJS 的缺点在于这样的代码无法直接运行在浏览器环境下

AMD

  1. AMD 也是一种 JavaScript 模块化规范,与 CommonJS 最大的不同在于它采用异步的方式去加载依赖的模块。 AMD 规范主要是为了解决针对浏览器环境的模块化问题,最具代表性的实现是 requirejs。

  2. AMD 的缺点在于JavaScript 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库后才能正常使用

ES6模块化

  1. 它在语言的层面上实现了模块化。浏览器厂商和 Node.js 都宣布要原生支持该规范。它将逐渐取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

  2. ES6模块虽然是终极模块化方案,但它的缺点在于目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行。

    1
    2
    3
    4
    5
    6
    7
    8
    // 导入
    import { readFile } from 'fs';
    import React from 'react';
    // 导出
    export function hello() {};
    export default {
    // ...
    };

样式文件中的模块

  1. 除了 JavaScript 开始模块化改造,前端开发里的样式文件也支持模块化。 以 SCSS 为例,把一些常用的样式片段放进一个通用的文件里,再在另一个文件里通过 @import 语句去导入和使用这些样式片段。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        // util.scss 文件

    // 定义样式片段
    @mixin center {
    // 水平竖直居中
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
    }

    // main.scss 文件

    // 导入和使用 util.scss 中定义的样式片段
    @import "util";
    #box{
    @include center;
    }

Vue

  1. Vue 框架把一个组件相关的 HTML 模版、JavaScript 逻辑代码、CSS 样式代码都写在一个文件里,这非常直观。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!--HTML 模版-->
    <template>
    <div class="example">{{ msg }}</div>
    </template>

    <!--JavaScript 组件逻辑-->
    <script>
    export default {
    data () {
    return {
    msg: 'Hello world!'
    }
    }
    }
    </script>

    <!--CSS 样式-->
    <style>
    .example {
    font-weight: bold;
    }
    </style>

TypeScript

  1. 是JavaScript的一个超集,支持ES6所有功能,还提供静态类型检查。需要编译才能转换成JavaScript。相对于JS,语法更严格,啰嗦。

SCSS

  1. 一种CSS预处理器,写完后需要编译成正常的CSS文件。Sass是老语法,SCSS是新语法,本质上时相同的。

常见的构建工具以及对比

  1. 以上的技术点都有一个共同点:源代码无法直接运行,必须通过转换才可以正常运行。

  2. 构建就是做这件事情,把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码:
    代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
    文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
    代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
    模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
    自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
    代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
    自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

  3. 构建工具:
    npm script:Npm Script 则是 Npm 内置的一个功能,允许在 package.json 文件里面使用 scripts 字段定义任务
    Grunt:Grunt 相当于进化版的 Npm Script,它的诞生其实是为了弥补 Npm Script 的不足。
    Gulp:Gulp 是一个基于流的自动化构建工具。 除了可以管理和执行任务,还支持监听文件、读写文件。可以将Gulp 看作 Grunt 的加强版。相对于 Grunt,Gulp增加了监听文件、读写文件、流式处理的功能。
    Fis3:相对于 Grunt、Gulp 这些只提供基本功能的工具,Fis3 集成了 Web 开发中的常用构建功能。不再更新。
    Webpack:Webpack 是一个打包模块化 JavaScript 的工具,在 Webpack 里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。Webpack 专注于构建模块化项目。Webpack 具有很大的灵活性,能配置如何处理文件。

  4. Webpack的优点是:
    专注于处理模块化的项目,能做到开箱即用一步到位;
    通过 Plugin 扩展,完整好用又不失灵活;
    使用场景不仅限于 Web 开发;
    社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
    良好的开发体验。
    Webpack的缺点是只能用于采用模块化开发的项目。

  5. Webpack 已经成为构建工具中的首选,这是有原因的:
    大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新架”,Webpack 可以为这些新项目提供一站式的解决方案;
    Webpack 有良好的生态链和维护团队,能提供良好的开发体验和保证质量;
    Webpack 被全世界的大量 Web 开发者使用和验证,能找到各个层面所需的教程和经验分享。

起步

安装 Webpack

npm install --save-dev webpack-cli webpack
对于大多数项目,我们建议本地安装。这可以使我们在引入破坏式变更(breaking change)的依赖时,更容易分别升级项目。

使用注意的问题

使用这种方式去管理 JavaScript 项目会有一些问题:
无法立即体现,脚本的执行依赖于外部扩展库(external library)。
如果依赖不存在,或者引入顺序错误,应用程序将无法正常运行。
如果依赖被引入但是并没有使用,浏览器将被迫下载无用代码。

将“源”代码(/src)从我们的“分发”代码(/dist)中分离出来。“源”代码是用于书写和编辑的代码。“分发”代码是构建过程产生的代码最小化和优化后的“输出”目录,最终将在浏览器中加载。

在安装一个要打包到生产环境的安装包时,你应该使用 npm install –save,如果你在安装一个用于开发环境的安装包(例如,linter, 测试库等),你应该使用 npm install –save-dev。

通过声明模块所需的依赖,webpack 能够利用这些信息去构建依赖图,然后使用图生成一个优化过的,会以正确顺序执行的 bundle。

可以这样说,执行 npx webpack,会将我们的脚本 src/index.js 作为 入口起点,也会生成 dist/main.js 作为 输出。Node 8.2/npm 5.2.0 以上版本提供的 npx 命令,可以运行在初始安装的 webpack 包(package)的 webpack 二进制文件(./node_modules/.bin/webpack)

概念

入口

入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始,webpack 会找出有哪些模块和 library 是入口起点(直接和间接)依赖的。默认值是 ./src/index.js,然而,可以通过在 webpack 配置中配置 entry 属性,来指定一个不同的入口起点(或者也可以指定多个入口起点)。

output

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,主输出文件默认为 ./dist/main.js,其他生成文件的默认输出目录是 ./dist。
你可以通过在配置中指定一个 output 字段,来配置这些处理过程。

loader

作为开箱即用的自带特性,webpack 自身只支持 JavaScript。而 loader 能够让 webpack 处理那些非 JavaScript 文件,并且先将它们转换为有效 模块,然后添加到依赖图中,这样就可以提供给应用程序使用。注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能。
在更高层面,在 webpack 的配置中 loader 有两个特征:

test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
use 属性,表示进行转换时,应该使用哪个 loader。

插件

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务,插件的范围包括:打包优化、资源管理和注入环境变量。

mode

通过将 mode 参数设置为 development, production 或 none,可以启用对应环境下 webpack 内置的优化。默认值为 production。

兼容浏览器

webpack 支持所有 ES5 兼容(IE8 及以下不提供支持)的浏览器。webpack 的 import() 和 require.ensure() 需要环境中有 Promise

模块

注意,webpack 不会转移代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack 的 loader 系统中使用了一个像是 Babel 或 Bublé 的转译器。

配置文件

在 webpack 4 中,可以无须任何配置使用,然而大多数项目会需要很复杂的设置,这就是为什么 webpack 仍然要支持 配置文件。这比在终端(terminal)中手动输入大量命令要高效的多,所以让我们创建一个取代以上使用 CLI 选项方式的配置文件.
如果 webpack.config.js 存在,则 webpack 命令将默认选择使用它。我们在这里使用 –config 选项只是向你表明,可以传递任何名称的配置文件。这对于需要拆分成多个文件的复杂配置是非常有用。
npx webpack --config webpack.config.js

管理资源

在 webpack 出现之前,前端开发人员会使用 grunt 和 gulp 等工具来处理资源,并将它们从 /src 文件夹移动到 /dist 或 /build 目录中。同样方式也被用于 JavaScript 模块,但是,像 webpack 这样的工具,将动态打包(dynamically bundle)所有依赖项(创建所谓的依赖图(dependency graph))。这是极好的创举,因为现在每个模块都可以_明确表述它自身的依赖,我们将避免打包未使用的模块。

webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件。也就是说,以上列出的那些 JavaScript 的优点(例如显式依赖),同样可以用来构建网站或 web 应用程序中的所有非 JavaScript 内容。让我们从 CSS 开始起步,或许你可能已经熟悉了这个设置过程。

加载CSS

为了从 JavaScript 模块中 import 一个 CSS 文件,你需要在 module 配置中 安装并添加 style-loader 和 css-loader:
npm install --save-dev style-loader css-loader

在这种情况下,以 .css 结尾的全部文件,都将被提供给 style-loader 和 css-loader。查看页面的 head 标签。它应该包含我们在 index.js 中导入的 style 块元素。注意,src中的css文件是不会放到dist中的。只会在head的style标签中出现。

最重要的是,现有的 loader 可以支持任何你可以想到的 CSS 处理器风格 - postcss, sass 和 less 等。

加载图片

使用 file-loader,我们可以轻松地将这些内容混合到 CSS 中。
npm install --save-dev file-loader
合乎逻辑下一步是,压缩和优化你的图像。可以看到dist目录中的图片名字以及style中的src属性都一并改变了。查看 image-webpack-loader 和 url-loader,以了解更多关于如果增强加载处理图片功能。

加载字体

file-loader 和 url-loader 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,包括字体。跟加载图片类似。

加载数据

此外,可以加载的有用资源还有数据,如 JSON 文件,CSV、TSV 和 XML。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from ‘./data.json’ 默认将正常运行。要导入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader。
npm install --save-dev csv-loader xml-loader

所导入的 Data 变量将包含可直接使用的已解析 JSON.

全局资源

上述所有内容中最出色之处是,以这种方式加载资源,你可以以更直观的方式将模块和资源组合在一起。无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起。例如,类似这样的结构会非常有用:

1
2
3
4
5
6
7
|- /assets
|– /components
| |– /my-component
| | |– index.jsx
| | |– index.css
| | |– icon.svg
| | |– img.png

这种配置方式会使你的代码更具备可移植性,因为现有的统一放置的方式会造成所有资源紧密耦合在一起。假如你想在另一个项目中使用 /my-component,只需将其复制或移动到 /components 目录下。只要你已经安装了任何扩展依赖(external dependencies),并且你已经在配置中定义过相同的 loader,那么项目应该能够良好运行。

管理输出

到目前为止,我们在 index.html 文件中手动引入所有资源,然而随着应用程序增长,并且一旦开始对文件名使用哈希(hash)]并输出多个 bundle,手动地对 index.html 文件进行管理,一切就会变得困难起来。然而,可以通过一些插件,会使这个过程更容易操控。

如果我们更改了我们的一个入口起点的名称,甚至添加了一个新的名称,会发生什么?生成的包将被重命名在一个构建中,但是我们的index.html文件仍然会引用旧的名字。我们用 HtmlWebpackPlugin 来解决这个问题。就像图片一样。
npm install --save-dev html-webpack-plugin

清理/dist文件夹

由于过去的指南和代码示例遗留下来,导致我们的 /dist 文件夹相当杂乱。webpack 会生成文件,然后将这些文件放置在 /dist 文件夹中,但是 webpack 无法追踪到哪些文件是实际在项目中用到的。通常,在每次构建前清理 /dist 文件夹,是比较推荐的做法,因此只会生成用到的文件。

webpack及其插件似乎“知道”应该哪些文件生成。答案是,通过 manifest,webpack 能够对「你的模块映射到输出 bundle 的过程」保持追踪。

开发

为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。

每次要编译代码时,手动运行 npm run build 就会变得很麻烦。

webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
webpack’s Watch Mode
webpack-dev-server
webpack-dev-middleware

观察模式

你可以指示 webpack “watch” 依赖图中的所有文件以进行更改。如果其中一个文件被更新,代码将被重新编译,所以你不必手动运行整个构建。

唯一的缺点是,为了看到修改后的实际效果,你需要刷新浏览器。如果能够自动刷新浏览器就更好了,可以尝试使用 webpack-dev-server,恰好可以实现我们想要的功能。【能对./dist目录编译生成】

使用web-dev-server

webpack-dev-server 为你提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading).如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码。【不会生成./dist,只有build才会生成】

模块热替换

  1. 它允许在运行时更新各种模块,而无需进行完全刷新。仅仅使用watch或者devServer是LiveReload(重新实时加载)。实际就是更新web-dev-server的配置,比如使用webpack内置的HMR插件以及使用hot模式。

tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。

你需要设置开发模式(development mode),来确保 bundle 是压缩过的(minified):

「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

将文件标记为无副作用(side-effect-free),如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。任何导入的文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并导入 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:

找出那些需要删除的“未使用代码(dead code)”,然而,我们不只是要找出,还需要在 bundle 中删除它们.通过 “mode” 配置选项轻松切换到压缩输出,只需设置为 “production”。

你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

为了学会使用 tree shaking,你必须……

使用 ES2015 模块语法(即 import 和 export)。
在项目 package.json 文件中,添加一个 “sideEffects” 属性。
引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。

生产环境构建

在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。【common,dev,prod以及重新设置npm scripts】

默认的代码压缩是uglifyJSPlugin

source map

source map:对调试源码(debug)和运行基准测试(benchmark tests)有帮助。
我们鼓励你在生产环境中启用 source map,因为它们对调试源码(debug)和运行基准测试(benchmark tests)很有帮助。虽然有如此强大的功能,然而还是应该针对生成环境用途,选择一个构建快速的推荐配置(具体细节请查看 devtool)。对于本指南,我们将在生产环境中使用 source-map 选项,而不是我们在开发环境中用到的 inline-source-map

指定环境

当使用 process.env.NODE_ENV === ‘production’ 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。

压缩CSS

将生产环境下的css进行压缩非常重要。

代码分离

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

有三种常用的代码分离方法:

入口起点:使用 entry 配置手动地分离代码。这种方式手动配置较多,并有一些陷阱,我们将会解决这些问题
防止重复:使用 SplitChunks 去重和分离 chunk。The SplitChunks 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
动态导入:通过模块的内联函数调用来分离代码。

webpack、gulp、grunt

首先我们要消除一个常见的误解。webpack 是一个模块打包器(module bundler)(例如,Browserify 或 Brunch)。它不是一个任务执行器(task runner)(例如,Make, Grunt 或者 Gulp )。任务执行器就是用来自动化处理常见的开发任务,例如项目的检查(lint)、构建(build)、测试(test)。相对于打包器(bundler),任务执行器则聚焦在偏重上层的问题上面。你可以得益于,使用上层的工具,而将打包部分的问题留给 webpack。